﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace NClass.TestHelper
{
	public static class MethodValidator
	{
		public enum Result
		{
			NullInstance,
			MethodNotFound,
			NullReference,
			Succeeded,
			UnknownFailure,
			UnexpectedResult
		}

		public static Result CheckParameterValidation<TInput>(TInput instance, string methodName, string testName,
			string testMethod, out string errorMessage, out object output, List<object> parameters = null)
		{
			return CheckParameterValidation<TInput, object, object>(instance, methodName, testName, testMethod,
				out errorMessage, out output, parameters, null, null);
		}

		public static Result CheckParameterValidation<TInput, TExpected, TOutput>(TInput instance, string methodName, string testName,
			string testMethod, out string errorMessage, out object output, List<object> parameters = null,
			object expectedValue = null, Func<TExpected, TOutput, bool> resultComparison = null)
		{
			var result = CheckParameterValidation<TInput, TExpected, TOutput>(instance, methodName, out output, parameters, expectedValue, resultComparison);
			errorMessage = FormatErrorMessage(result, testName, testMethod, typeof(TInput), methodName, output, expectedValue);
			return result;
		}

		private static Result CheckParameterValidation<TInput, TExpected, TOutput>(TInput instance, string methodName, out object output,
			List<object> parameters = null, object expectedValue = null, Func<TExpected, TOutput, bool> resultComparison = null)
		{
			output = null;
			if (instance == null)
				return Result.NullInstance;

			if (string.IsNullOrWhiteSpace(methodName))
				throw new ArgumentNullException();

			var methodInfo = (typeof(TInput)).GetMethod(methodName,
				BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

			if (methodInfo == null)
				return Result.MethodNotFound;

			try
			{
				if (parameters == null)
				{
					parameters = new List<object>();

					foreach (var paramInfo in methodInfo.GetParameters())
					{
						parameters.Add(GetDefaultValue(paramInfo));
					}
				}

				output = methodInfo.Invoke(instance, parameters.ToArray());

				return resultComparison == null || resultComparison((TExpected)expectedValue, (TOutput)output) ?
					Result.Succeeded : Result.UnexpectedResult;
			}
			catch (TargetInvocationException targetInvoEx)
			{
				if (targetInvoEx.InnerException is NullReferenceException)
					return Result.NullReference;
				else if (targetInvoEx.InnerException is ArgumentNullException)
					return Result.Succeeded;
				else
					throw;
			}

			return Result.Succeeded;
		}

		private static object GetDefaultValue(ParameterInfo paramInfo)
		{
			if (paramInfo == null)
				return null;
			if (!(paramInfo.DefaultValue is System.DBNull))
				return paramInfo.DefaultValue;
			return GetDefaultValue(paramInfo.ParameterType);
		}

		private static object GetDefaultValue(Type type)
		{
			// This implementation needs to be completed to actually return
			// the default value of the type. Null could cause an exception
			// for a non-nullable type
			return null;
		}

		private static string FormatErrorMessage(Result result, string testName, string testMethod, Type targetType,
			string targetMethod, object actualResult = null, object expectedResult = null)
		{
			if (!targetMethod.Contains("("))
			{
				targetMethod += "()";
			}

			switch (result)
			{
				case Result.NullInstance:
					return string.Format("{0}.{1}() -> Test failed because no instance of type '{2}' was provided.",
						testName, testMethod, targetType.Name);
				case Result.MethodNotFound:
					return string.Format("{0}.{1}() -> Test failed because the method {2}.{3} could not be found.",
						testName, testMethod, targetType.Name, targetMethod);
				case Result.NullReference:
					return string.Format("{0}.{1}() -> Test failed because the method {2}.{3} did not correctly handle default parameters. Parameter validation is needed.",
						testName, testMethod, targetType.Name, targetMethod);
				case Result.UnexpectedResult:
					return string.Format("{0}.{1}() -> Test failed because the method {2}.{3} retured the value of {4} instead of the expected value of {5}",
						testName, testMethod, targetType.Name, targetMethod,
						actualResult == null ? "{NULL}" : actualResult.ToString(),
						expectedResult == null ? "{NULL}" : expectedResult.ToString());
				default:
					return string.Empty;
			}
		}

		public static Result CheckParameterValidation<TInput>(Action method, string methodName, string testName, string testMethod, out string errorMessage)
		{
			if (method == null)
				throw new ArgumentNullException("MethodValidator.CheckParameterValidation() requires a method to be specified");

			var result = Result.UnknownFailure;

			try
			{
				method.Invoke();
			}
			catch (TargetInvocationException targetInvoEx)
			{
				if (targetInvoEx.InnerException is NullReferenceException)
					result = Result.NullReference;
				else if (targetInvoEx.InnerException is ArgumentNullException)
					result = Result.Succeeded;
				else
					throw;
			}
			catch (NullReferenceException)
			{
				result = Result.NullReference;
			}
			catch (ArgumentNullException)
			{
				result = Result.Succeeded;
			}

			errorMessage = FormatErrorMessage(result, testName, testMethod, typeof(TInput), methodName);

			return result;
		}
	}
}
